-
Notifications
You must be signed in to change notification settings - Fork 178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Escape username within DN correctly and remove escape_userdn
#267
Escape username within DN correctly and remove escape_userdn
#267
Conversation
4f7bd11
to
242ff07
Compare
escape_userdn
242ff07
to
1c254c3
Compare
escape_userdn
escape_userdn
@m-erhardt, @Nikolai-Hlubek, @manics, @minrk if you have capacity to review this a bit, I'd greatly appreciate it - this isn't a PR I feel comfortable merging without further input. I think this is a crucial fix for the reliability and long term maintenance of this project, but I've not really tested the behavior changes from this change in any other setup than the CI system which doesn't have great coverage of configurations and LDAP servers. |
1c254c3
to
0cfd040
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On my reading of everything, this looks sensible to me, but I've never used LDAP. Thanks for doing all this work!
conn = self.get_connection( | ||
userdn=search_dn, | ||
userdn=self.lookup_dn_search_user, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that the DN here isn't escaped with escape_rdn, because its a full DN rather than a part of it. The full DN include commas and equalsigns for example, and they shouldnt be escaped as they are describing structure of the DN itself, but an individual attributes value should be escaped.
We cant do that here though as we have a full DN here and not its parts.
0cfd040
to
fee33e2
Compare
(Rebased on main) |
Going for a merge and aiming for this to be available to test practically in a beta release |
@@ -463,17 +462,12 @@ async def authenticate(self, handler, data): | |||
username, resolved_dn = self.resolve_username(username) | |||
if not username: | |||
return None | |||
if str(self.lookup_dn_user_dn_attribute).upper() == "CN": | |||
# Only escape commas if the lookup attribute is CN | |||
username = re.subn(r"([^\\]),", r"\1\,", username)[0] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The later escape_rdn is a superset of this comma escape. So probably ok.
However the if ... logic is now missing. Is it important to only do this escape at some times?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The if logic didn't make sense to me, the username should reasonably always be escaped before being passed to the LDAP server.
Hmmm, i guess this could also have impacted the username chosen if use_lookup_dn_username is true as well though...
Are you a user of that?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for my use case the if condition would always be true as my company uses Active Directory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, the issue I'm seeing now, is that username
is assigned with the escaping, and the username variable will, conditionally on use_lookup_dn_username
end up being used to become the JupyterHub username, which otherwise can become whatever the user initially wrote in the login prompt.
This is because of this line of code, where resolved_username
seen below was previously named username
. In other words, the use_lookup_dn_username
can be used or not and it doesn't really matter for anything except the resulting JupyterHub username. One doesn't want to change the username for one person over time typically though, as storage can be associated with the JupterHub username.
ldapauthenticator/ldapauthenticator/ldapauthenticator.py
Lines 642 to 647 in cea8616
username = resolved_username if self.use_lookup_dn_username else login_username | |
auth_state = { | |
"ldap_groups": ldap_groups, | |
"user_attributes": user_attributes, | |
} | |
return {"name": username, "auth_state": auth_state} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I updated the PR description with this:
EDIT: Breaking change
This PR introduced a breaking change in a edge case scenario if both...
- the following LDAPAuthenticator configuration was used:
lookup_dn = True
,lookup_dn_user_dn_attribute = "cn"
,use_lookup_dn_username = True
(previous default value) - one or more users, that had already signed in at any point in time before, had a comma in the cn attribute's value, as they would if for example the cn attribute values looks like "lastname, firstname"
Then, these users' resulting JupyterHub usernames would be changed from looking like "lastname\\, firstname"
to "lastname, firstname"
.
userdn = dn.format(username=username) | ||
if self.escape_userdn: | ||
userdn = escape_filter_chars(userdn) | ||
userdn = dn.format(username=escape_rdn(username)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
escape_filter_chars and escape_rdn seem to escape for different things unless I'm missing something.
escape_filter_chars(r"\foo + , (N") -> '\\5cfoo + , \\28N'
escape_rdn(r"\foo + , (N") -> '\\\\foo \\+ \\, (N'
Is it ok to exchange them here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes i think that is right to do, because the rdn escape is for parts of a dn like here. Filter chars wasn't right to use here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a comment to this in another PR merged after this, linking to an RFC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you are correct. You explained this at the beginning of the PR but first I didn't understand this.
@consideRatio Sorry for the late reply. |
I know, I'm a little late to the party. As more testing/validation certainly wont hurt before the release of 2.0.0 I've just validated this change by testing the current version from the c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
c.LDAPAuthenticator.server_address = 'our.companys.ldap.server'
c.LDAPAuthenticator.server_port = 636
c.LDAPAuthenticator.use_ssl = True
c.LDAPAuthenticator.lookup_dn = True
c.LDAPAuthenticator.lookup_dn_search_filter = '({login_attr}={login})'
c.LDAPAuthenticator.lookup_dn_search_user = 'CN=jupyter.bind,OU=Users,DC=our,DC=company'
c.LDAPAuthenticator.lookup_dn_search_password = '***'
c.LDAPAuthenticator.user_search_base = 'DC=our,DC=company'
c.LDAPAuthenticator.user_attribute = 'sAMAccountName'
c.LDAPAuthenticator.lookup_dn_user_dn_attribute = 'cn'
c.LDAPAuthenticator.escape_userdn = False
c.LDAPAuthenticator.use_lookup_dn_username = False
c.LDAPAuthenticator.allowed_groups = [
"CN=JupyterHubGroup,OU=Groups,DC=our,DC=company",
] I've also validated that the issue described in #237 is still fixed (which is the case). So thanks @consideRatio for decluttering the whole So for most users using ldapauthenticator with ActiveDirectory this should not be a breaking change. |
Background
Why
escape_userdn
was introducedescape_userdn
was introduced after a non-conclusive discussion in #32 (review), where it made things break for some, and fixed things for others.I think that was a fluke related to other issues, such as using the
userdn
variable both in a binding operation and in other operations where it was part of a search filter. When a DN is part of a search filter as a value to find, it should be escaped withescape_filter_chars
. With #238, we are good at usingescape_filter_chars
just-in-time where its needed.Prevent LDAP injections and the
escape_rdn
functionThe config
valid_username_regex
was this projects key defence for LDAP injection attacks, but since it was introduced there is now aescape_rdn
function to help escaping usernames given to be part of a DN viabind_dn_template
strings. This is stressed as important in the ldap3 Python package's docs:What should be escaped?
escape_rdn
).This is described in https://datatracker.ietf.org/doc/html/rfc4514#section-2.4. Escaped characters are
\,+"<>;=
(and null).escape_filter_chars
).This is described in https://datatracker.ietf.org/doc/html/rfc4515#section-3. Escaped characters are
/()*
(and null).Recent fixes related to this are in Escape user- or ldap-provided strings in ldap search filters #238.
PR changes
escape_userdn
is made to no longer have any effect and logs a warning if setescape_rdn
is applied on a username string when constructing DN(s) frombind_dn_template
.,
in a username, which was just a piece of whatescape_rdn
escapes.EDIT: Breaking change
This PR introduced a breaking change in a edge case scenario if both...
lookup_dn = True
,lookup_dn_user_dn_attribute = "cn"
,use_lookup_dn_username = True
(previous default value)Then, these users' resulting JupyterHub usernames would be changed from looking like
"lastname\\, firstname"
to"lastname, firstname"
.Related
escape_userdn
doesn't seem right - is it? #266